﻿
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;

namespace EmperialApps.WeatherSpark.Data {

    /// <summary>Represents a geo-located place.</summary>
    public partial struct Place {

        /// <summary>Gets the URI address separator.</summary>
        public const char UriSeparator = '|';

        /// <summary>Gets the address ID separator.</summary>
        public const char IdSeparator = '~';

        /// <summary>Gets the address prefix for yr.no forecast links.</summary>
        public const string YrnoAddress = "http://www.yr.no/place/";

        /// <summary>Gets the full name of the city.</summary>
        public string FullName {
            get {
                return string.IsNullOrEmpty( this.County )
                     ? this.Name
                     : this.Name + " (" + this.County + ")";
            }
        }

        /// <summary>Gets the name of the closest city, without distance indicator.</summary>
        public string CityName {
            get {
                int separatorIndex = this.Name.IndexOf( DistanceSeparator );
                return separatorIndex < 0
                     ? this.Name
                     : this.Name.Substring( 0, separatorIndex );
            }
        }


        /// <summary>Creates a place with the specified values, correcting the punctuation around the state if necessary.</summary>
        public static Place Create( Units units, Coordinate location, string name, string county = "" ) {
            // Separate link from name, if present.
            string link;
            int linkIndex = name.IndexOf( UriSeparator );
            if( linkIndex < 0 ) {
                link = null;
            }
            else {
                link = name.Substring( 1 + linkIndex );
                name = name.Substring( 0, linkIndex );
            }

            // Insert comma between city and state name, if missing.
            int length = name.Length;
            if( length > 3
             && char.IsLower( name, length - 4 )
             && char.IsWhiteSpace( name, length - 3 )
             && char.IsUpper( name, length - 2 )
             && char.IsUpper( name, length - 1 ) )
                name = name.Substring( 0, name.Length - 3 ) + "," + name.Substring( length - 3 );

            // Beautify "close to city" places.
            Match distanceMatch = Distance.Match( name );
            if( distanceMatch.Success ) {
                double distance = double.Parse( distanceMatch.Groups[1].Value );
                string direction = distanceMatch.Groups[2].Value;
                string city = distanceMatch.Groups[3].Value;
                if( units == Units.Metric )
                    distance = ConvertValue.ToKilometers( distance );

                name = string.Format( "{0}{1} {2} {3:0}\u200a{4}", city, DistanceSeparator, direction, distance, units == Units.Metric ? "km" : "mi" );
            }

            return new Place( location, name, county, link );
        }

        /// <summary>Reads the cities contained in the specified stream.</summary>
        public static Place[] Load( Units units, Stream source, Corrections corrections ) {
            return LoadCore( units, source, corrections ).ToArray( );
        }

        /// <inheritdoc/>
        public override string ToString( ) {
            string link = string.IsNullOrEmpty( this.Link ) ? "" : " @" + this.Link.Substring( YrnoAddress.Length - 1 ).TrimEnd( '/' );
            return this.Location + ": " + this.FullName + link;
        }


        /// <summary>Represents a set of corrections to apply to search values during load.</summary>
        public struct Corrections {
            private const char ItemSeparator = '|';
            private const char PairSeparator = '$';
            private const char SkipIndicator = '&';
            private const char CountrySeparator = '%';

            private static readonly Dictionary<string, byte> DefaultPartFilter = new Dictionary<string, byte>( StringComparer.OrdinalIgnoreCase ) {
                { "City", byte.MaxValue },
                { "County", 0 },
                { "Departamento", 1 },
                { "District", 0 },
                { "Estado", 0 },
                { "Metropolitan", 0 },
                { "Municipality", 0 },
                { "Parish", 0 },
                { "Province", 0 },
                { "Provincia", 1 },
                { "Region", 0 },
                { "State", 0 },
            };
            private static readonly Dictionary<Pair<string, string>, string> DefaultLinkFilter = new Dictionary<Pair<string, string>, string> {
                { Pair.Create( "BE", "Walloon" ), "Wallonia" },
                { Pair.Create( "CO", "Bogota D.C." ), "Bogotá" },
            };

            private readonly Dictionary<Pair<string, string>, string> _linkFilter;
            private readonly Dictionary<string, byte> _partFilter;

            private Corrections( Dictionary<string, byte> partFilter, Dictionary<Pair<string, string>, string> linkFilter ) {
                this._partFilter = partFilter;
                this._linkFilter = linkFilter;
            }

            private static Pair<string, string> SeparateAtCharacter( string value, char separator ) {
                int index = value.IndexOf( separator );
                return Pair.Create(
                    value.Substring( 0, index ),
                    value.Substring( 1 + index, value.Length - index - 1 ) );
            }


            /// <summary>Saves the current set of corrections to a serialized string value.</summary>
            public string Save( ) {
                var partFilter = this._partFilter ?? DefaultPartFilter;
                var linkFilter = this._linkFilter ?? DefaultLinkFilter;
                var parts = partFilter.Select( kvp => kvp.Key + PairSeparator + SkipIndicator + kvp.Value );
                var links = linkFilter.Select( kvp => kvp.Key.Item1 + CountrySeparator + kvp.Key.Item2 + PairSeparator + kvp.Value );

                string[] values = parts.Concat( links ).ToArray( );
                string serialized = string.Join( ItemSeparator.ToString( ), values );
                return serialized;
            }

            /// <summary>Loads a set of corrections from a serialized string value.</summary>
            public static Corrections Load( string value ) {
                var partFilter = new Dictionary<string, byte>( );
                var linkFilter = new Dictionary<Pair<string, string>, string>( );

                string[] items = (value ?? "").Split( ItemSeparator );
                foreach( string item in items ) {
                    string filter, result;
                    SeparateAtCharacter( item, PairSeparator ).GetValues( out filter, out result );

                    byte partResult;
                    if( result[0] == SkipIndicator && byte.TryParse( result.Substring( 1 ), out partResult ) )
                        partFilter[filter] = partResult;
                    else
                        linkFilter[SeparateAtCharacter( filter, CountrySeparator )] = result;
                }

                return new Corrections( partFilter, linkFilter );
            }


            /// <summary>Gets the number of loaded corrections, or zero if this is the default set of corrections.</summary>
            public int LoadCount {
                get {
                    return this._partFilter == null || this._linkFilter == null
                         ? 0
                         : this._partFilter.Count + this._linkFilter.Count;
                }
            }


            /// <summary>Determines whether the current part in a name, and the next <paramref name="skip"/> number of parts, should be ignored.</summary>
            public bool SkipPart( string part, out byte skip ) {
                var partFilter = this._partFilter ?? DefaultPartFilter;
                return partFilter.TryGetValue( part, out skip );
            }

            /// <summary>Applies any required corrections to the first administrative name in a link.</summary>
            public void ForLink( string countryCode, ref string adminName1 ) {
                var linkFilter = this._linkFilter ?? DefaultLinkFilter;

                string correction;
                if( linkFilter.TryGetValue( Pair.Create( countryCode, adminName1 ), out correction ) )
                    adminName1 = correction;
                else if( string.IsNullOrEmpty( adminName1 ) )
                    adminName1 = "Other";
            }
        }


        #region Private Members

        private const char DistanceSeparator = ';';

        private static readonly char[] NumberCharacters = "0123456789".ToCharArray( );
        private static readonly Regex Distance = new Regex( @"(\d+) Miles (\w+) (.+)", RegexOptions.IgnoreCase );

        private static IEnumerable<Place> LoadCore( Units units, Stream source, Corrections corrections ) {
            var excluded = new List<Place>( );
            Place lastPlace = default( Place );
            var reader = XmlReader.Create( new XmlSanitizingStream( source ) );
            while( reader.ReadToFollowing( "geoname" ) ) {
                string name = ReadElementString( reader, "name" ).Replace( " (historical)", "" );

                double latitude = ReadElementNumber( reader, "lat" );
                double longitude = ReadElementNumber( reader, "lng" );
                Coordinate location = new Coordinate( latitude, longitude );

                string id = ReadElementString( reader, "geonameId" );
                string countryCode = ReadElementString( reader, "countryCode" );
                string countryName = ReadElementString( reader, "countryName" );

                reader.ReadToFollowing( "adminCode1" );
                string adminCode1 = reader.GetAttribute( "ISO3166-2" ) ?? "0";
                string adminName1 = ReadElementString( reader, "adminName1", corrections );
                if( adminCode1.IndexOfAny( NumberCharacters ) >= 0 )
                    adminCode1 = adminName1;

                string adminName2 = ReadElementString( reader, "adminName2", corrections );
                if( adminName2.IndexOfAny( NumberCharacters ) >= 0 )
                    adminName2 = "";

                string placeName = string.Join( ", ", name, adminCode1, countryCode ).Replace( ", ,", "," );
                string link = GetLink( name, id, countryCode, countryName, adminName1, adminName2, corrections );

                Place place = new Place( location, placeName, adminName2, link );
                bool repeat = place.FullName == lastPlace.FullName && place.County == lastPlace.County;
                if( !repeat ) {
                    if( !name.StartsWith( "City of " ) ) {
                        excluded = null;
                        yield return place;
                    }
                    else if( excluded != null ) {
                        excluded.Add( place );
                    }
                }

                lastPlace = place;
            }

            foreach( Place place in excluded ?? Enumerable.Empty<Place>( ) )
                yield return place;
        }

        private static string GetLink( string name, string id, string countryCode, string countryName, string adminName1, string adminName2, Corrections corrections ) {
            // Correct known differences between geo and yr.no names.
            corrections.ForLink( countryCode, ref adminName1 );

            // Only Norway uses District names in path.
            string uniqueName = name + IdSeparator + id;
            string[] parts =
                countryCode == "NO"
                    ? new[] { countryName, adminName1, adminName2, uniqueName }
                    : new[] { countryName, adminName1, uniqueName };

            string path = string.Join( "/", parts ).Replace( ' ', '_' );
            string link = YrnoAddress + path + "/";
            return link;
        }

        private static double ReadElementNumber( XmlReader reader, string name ) {
            reader.ReadToFollowing( name );
            return reader.ReadElementContentAsDouble( );
        }

        private static string ReadElementString( XmlReader reader, string name, Corrections? corrections = null ) {
            reader.ReadToFollowing( name );
            string value = reader.ReadElementContentAsString( );

            if( corrections.HasValue ) {
                string[] parts = value.Split( );
                if( parts.Length > 4 ) {
                    value = "";
                }
                else if( parts.Length > 1 ) {
                    Corrections c = corrections.Value;

                    int count = 0;
                    for( int i = 0; i < parts.Length; ++i ) {
                        string part = parts[i];

                        byte skip;
                        if( c.SkipPart( part, out skip ) )
                            i += skip;
                        else
                            parts[count++] = part;
                    }

                    value = string.Join( " ", parts, 0, count );
                }
            }

            return value;
        }

        #endregion
    }

}
